package cn.haocxy.jfm.file;

import java.awt.Desktop;
import java.io.IOException;
import java.nio.file.FileSystems;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;

import cn.haocxy.jfm.util.OS;

/**
 * 文件路径
 * 
 * 包装java.nio.Path，封装本软件要用到的功能。
 * <p>
 * 功能定位类似于Path类
 * <p>
 * 本类的对象为不可变对象 。
 * 
 * @author 蔡心宇
 *
 */
public class FilePath {

	/*
	 * 本软件把Windows的根路径定义为显示盘符的那个界面，类似于“我的电脑”，所以无法用普通路径表示，只能用这种特殊标识的形式。
	 */
	private static final Path WIN32ROOT_PATH = Paths
			.get("^_^I am only for windows!");

	private static final FilePath WIN32_ROOT = new FilePath(WIN32ROOT_PATH);

	private static final FilePath UNIX_ROOT = new FilePath(Paths.get("/"));

	private final Path path;

	private FilePath(Path path) {
		if (path == null) {
			throw new NullPointerException("path cannot be null.");
		}
		// 为了简化逻辑，所有路径都转化为绝对路径
		this.path = path.toAbsolutePath();
	}

	public Path toPath() {
		return path;
	}

	/**
	 * 判断是否是Windows的根路径
	 * <p>
	 * 因为Windows本地磁盘路径设计比较特殊，很多操作需要判断一个路径是否是Windows根(盘符界面)
	 * <p>
	 * 本软件把Windows的跟路径定义为内容为磁盘分区的那个路径，
	 * 也就是我们打开"我的电脑"时显示的那个界面。
	 * <p>
	 * 对于普通用户来说，我相信这样的思路更符合人们在Windows下对
	 * 根路径的理解。
	 * 
	 * @return true-是Windows根路径，false-不是Windows根路径
	 */
	public boolean isWin32Root() {
		return WIN32_ROOT.equals(this);
	}

	/**
	 * 打开文件(不包括文件夹，文件夹的打开由GUI部分处理)
	 * <p>
	 * 对于不同的操作系统，效果不同，但都会提供一个供用户查看或编辑文件内容的方式。
	 * 编辑功能不一定会提供，取决于人们通常编辑该文件类型的概率。如对于txt文件或html文件，
	 * 一定会提供编辑功能，但是对于MP3文件，不会由软件自身提供，具体情况则取决于操作系统类型。
	 * <p>
	 * 对于Windows这种以GUI为中心的操作系统，软件会把打开文件的任务直接交给操作系统。
	 * <p>
	 * 而对于Linux这种GUI功能非常混乱的系统，软件会为最常用的文件类型提供在软件内部的打开。
	 * 如打开一个本软件创建的窗口或面板。当然，这时仅会提供比较简单的查看或编辑功能。
	 * <p>
	 * 在软件比较成熟以后，会以插件的形式实现对复杂文件类型的支持。
	 */
	public void open() {

		/*
		 * 调用操作系统打开文件只为Windows提供，Linux上支持得不好。
		 * 在Windows之外的操作系统上通过在软件内部为常见格式（MP3、TXT等）提供简单支持来弥补。
		 */
		if (OS.type() == OS.Windows) {
			try {
				Desktop.getDesktop().open(path.toFile());
			} catch (Exception ex) {
				throw new RuntimeException(ex.getMessage(), ex);
			}
		}

		// 稍后实现其他系统的文件打开，暂时什么都不做，稍后实现
		// TODO
	}

	/**
	 * 获取文件名
	 * <p>
	 * 对于Windows的盘符，会返回盘符本身，如对于"C:\"就会返回"C:\";<br>
	 * 对于文件夹，会返回文件夹的名字;<br>
	 * 对于文件，返回文件的名字。
	 * 
	 * @return 路径所代表的文件(文件夹)的名字
	 */
	public String getFileName() {
		String s = path.toString();
		if (OS.isWin32() && s.length() <= 3) {
			return s;
		} else {
			return path.getFileName().toString();
		}
	}

	/**
	 * 
	 * @return true - 是文件夹或Windows分区(如"C:\");<br>
	 *         false- 不是文件夹
	 */
	public boolean isDirectory() {
		return Files.isDirectory(path) || equals(WIN32_ROOT);
	}

	/**
	 * 返回文件类型，注意，文件夹也被看作文件的一种。
	 * 
	 * @return 文件类型
	 */
	public FileType getFileType() {
		if (OS.isWin32() && toString().length() <= 3) {
			return FileType.WIN_PART;
		}
		if (isDirectory()) {
			return FileType.FOLDER;
		}
		String name = getFileName();
		if (name.endsWith(".html") || name.endsWith(".htm")) {
			return FileType.HTML;
		}
		if (name.endsWith(".exe")) {
			return FileType.EXE;
		}
		if (isWin32Root()) {
			return FileType.WIN_ROOT;
		}
		return FileType.UNKNOWN;
	}

	/**
	 * 当前路径所代表的文件是否属有父路径。
	 * <p>
	 * 对于Windows，因为把分区界面看作根路径，所以，
	 * 如果当前路径为"C:\"，则有父路径，返回true。
	 * 
	 * 
	 * @return true-有父路径
	 */
	public boolean hasParent() {
		if (OS.isWin32()) {
			return !equals(WIN32_ROOT);
		} else {
			return path.getParent() != null;
		}
	}

	/**
	 * 返回当前路径的父路径，如果当前路径没有父路径，则返回null。
	 * 使用此方法前，请务必使用hasParent判断是否有父路径，否则
	 * null作为路径会导致奇怪的行为。
	 * 
	 * @return 父路径，没有则返回null。
	 */
	public FilePath getParent() {
		Path par = path.getParent();
		if (par != null) {
			return new FilePath(par);
		}
		if (OS.isWin32() && !equals(WIN32_ROOT)) {
			return WIN32_ROOT;
		}
		return null;
	}

	/**
	 * 获取当前操作系统的根路径。
	 * <p>
	 * 把Windows的跟路径定义为内容为磁盘分区的那个路径， 也就是我们打开"我的电脑"时显示的那个界面。
	 * 
	 * @return 当前操作系统的根路径。
	 */

	public FilePath getRoot() {
		if (OS.isWin32()) {
			return WIN32_ROOT;
		} else {
			return UNIX_ROOT;
		}
	}

	/**
	 * 返回当前目录下的所有子路径。
	 * 
	 * @return 当前目录下的所有子路径
	 * @throws IOException
	 */
	public Collection<FilePath> getChildren() {
		Collection<FilePath> res = new ArrayList<>();
		if (equals(WIN32_ROOT)) {
			FileSystems.getDefault().getRootDirectories().forEach(e -> {
				res.add(new FilePath(e));
			});
		} else {
			try {
				Files.list(path).forEach(e -> {
					res.add(new FilePath(e));
				});
			} catch (IOException e) {
				throw new RuntimeException(e);
			}
		}
		return res;
	}

	public String toString() {
		if (equals(WIN32_ROOT)) {
			return "\\";
		}
		return path.toString();
	}

	public int hashCode() {
		return path.hashCode();
	}

	public boolean equals(Object obj) {
		if (this == obj)
			return true;
		if (obj == null)
			return false;
		if (getClass() != obj.getClass())
			return false;
		FilePath other = (FilePath) obj;
		if (path == null) {
			if (other.path != null)
				return false;
		} else if (!path.equals(other.path))
			return false;
		return true;
	}

	/**
	 * 使用传入的路径字符串构建一个文件路径对象，对路径进行必要的转换处理。
	 * <p>
	 * 对于UNIX类操作系统，原样保留。
	 * <p>
	 * 对于Windows，因为其别扭的盘符设计，会按照以下规则处理：
	 * <p>
	 * 在进行所有处理前，先把路径中的所有斜杠，包括"/"和"\"都替换成Windows风格的"\"，
	 * 然后把所有多个连续的斜杠替换成一个斜杠，把此时的字符串记为s。之后分情况处理：
	 * <ul>
	 * <li>当s长度为1并且为一个斜杠，这被认为用户要表示的是Windows的根路径，即显示盘符的那个界面。
	 * 这时将返回一个已经初始化好的代表Windows根路径的特殊路径。
	 * <li>当s长度为2，内容为斜杠后面根一个盘符(大小写均可)，形如"\C"或"\d"，这时会转换为形如 "C:\"或“D:\"的形式。
	 * <li>当s长度大于等于3，且内容为 斜杠+盘符+斜杠[+其他]，形如"\c\"或"\d\a"，这时会转换为 形如"C:\"或"D:\a"的形式。
	 * <li>不在以上3种情况的路径也原样保留。
	 * </ul>
	 * 
	 * @param path
	 *            用户输入的路径
	 * @return 生成的路径对象。
	 */
	public static FilePath fromString(String path) {

		/*
		 * 对于Windows系统，检测传入的路径的几种特殊情况， 不属于特殊情况的由Path类处理。
		 * ---------------------------------------------------------- 首先，作处理：
		 * (1)把路径斜杠的风格统一为Windows风格 (2)连续的斜杠保留一个
		 * ----------------------------------------------------------
		 * 然后，以下三种情况区别处理：
		 * 
		 * (1)路径长度为1 1:只有一个斜杠，形如"\"。 这表示类似于打开"我的电脑"时最初显示的那个界面，
		 * 内容为磁盘分区列表，本软件把显示磁盘分区的这个 界面看作Windows下文件系统树的根。
		 * 这种情况的处理方法是直接返回一个已经初始化好的常量对象作标志。
		 * 
		 * (2)路径长度为2 1:一个斜杠后根着一个Windows盘符，形如"\C"或"\d"。 显然，这表示一个磁盘分区的根目录。
		 * 处理方式为将其转化为形如"C:\"或"D:\"后交给Path处理。 处理时要注意把盘符转为大写字母。
		 * 2:一个英文字母加一个冒号,形如"C:",转换为"C:\"后交给Path类处理.
		 * 
		 * 
		 * (3)路径长度大于等于3，一个斜杠跟着一个Windows盘符，再跟一个斜杠, 形如"\C\a.txt"或"\e\"。
		 * 这种情况会被转化为"C:\a.txt"或"E:\"再交给Path类处理。 处理时要注意把盘符转为大写字母。
		 */
		if (OS.isWin32()) {
			path = path.replaceAll("/", "\\\\"); // 统一为Windows风格斜杠("/" -> "\")
			path = path.replaceAll("\\\\+", "\\\\"); // 去掉所有连续多个斜杠("\\+" -> "\")
			int len = path.length();

			char[] arr = path.toCharArray();

			// length == 1
			if (len == 1) {
				char ch = Character.toUpperCase(arr[0]);
				if (ch == '\\') {
					return WIN32_ROOT;
				}
			}

			// length == 2
			else if (len == 2) {
				char ch0 = Character.toUpperCase(arr[0]);
				char ch1 = Character.toUpperCase(arr[1]);
				boolean bo1 = (ch0 == '\\');
				boolean bo2 = (ch1 >= 'A' && ch1 <= 'Z');
				if (bo1 && bo2) {
					path = ch1 + ":\\";
					return new FilePath(Paths.get(path));
				}
				if (ch0 >= 'A' && ch0 <= 'Z' && ch1 == ':') {
					return new FilePath(Paths.get(path + "\\"));
				}
			}

			// length >= 3
			// 注意!! 这里必须else if指明范围，因为还有length == 0的情况不做特殊处理
			else if (len >= 3) {
				char ch0 = arr[0];
				char ch1 = Character.toUpperCase(arr[1]);
				char ch2 = arr[2];
				boolean bo1 = (ch0 == '\\' && ch2 == '\\');
				boolean bo2 = (ch1 >= 'A' && ch1 <= 'Z');
				if (bo1 && bo2) {
					path = ch1 + ":" + path.substring(2);
					return new FilePath(Paths.get(path));
				}
			}
		}

		// 非Windows操作系统的路径及Windows系统下的非特殊路径直接由Path类处理
		return new FilePath(Paths.get(path));
	}

	/**
	 * <p>是否是隐藏文件
	 * <p>对于文件系统不提供隐藏功能的, 把以'.'开头的文件视为隐藏文件.
	 * @return true-隐藏文件
	 */
	public boolean isHidden() {
		if (OS.isWin32()) {
			try {
				return Files.isHidden(path);
			} catch (Exception e) {
				return false;
			}
		} else if (OS.isLinux()) {
			return path.getFileName().toString().charAt(0) == '.';
		} else {
			throw new RuntimeException("implement me!");
		}
	}
	
	/**
	 * <p>
	 * 该文件路径能否打开
	 * <p>
	 * 打开的含义随文件类型而变化
	 * <p>
	 * 对于文件夹,则表示当前用户是否有权限查看该文件夹的内容,即进入文件夹或切换到该路径.
	 * <p>
	 * 对于普通文件,则表示当前用户是否有对该文件的读或写权限.
	 * <p>
	 * 对于可执行文件,则表示当前用户是否可以执行该文件.
	 * @return
	 */
	public boolean canOpen() {
		if (isWin32Root()) {
			return true;
		}
		if (isDirectory()) {
			
			if (OS.isWin32()) {
				
				return isReadable();
				
			} else  if (OS.isLinux()){
				
				return isReadable() && isExecutable();
			} else {
				// TODO
				throw new RuntimeException("implement me!");
			}
			
		} else {
			
			if (isExecutable()) {
				return true;
			} else {
				return isReadable() || isWritable();
			}
		}
		
	}
	
	public boolean isExecutable() {
		return Files.isExecutable(path);
	}
	
	public boolean isWritable() {
		return Files.isWritable(path);
	}
	
	public boolean isReadable() {
		return Files.isReadable(path);
	}
	
	/**
	 * 检查这个对象是否是可访问的目录
	 */
	public void checkDirAccessable() {
		if (this.isWin32Root()) {
			return;
		}
		if (!isDirectory()) {
			throw new RuntimeException("'" + toString() + "' is not a directory.");
		}
		if (!Files.isReadable(path)) {
			throw new RuntimeException("Access denied :'" + path + "'");
		}
	}

	public void rename(String newName) {
		Path parentPath = path.getParent();
		Path target = parentPath.resolve(newName);
		try {
			Files.move(path, target);
		} catch (Exception e) {
			throw new RuntimeException(e);
		}
	}
	
	public boolean isWin32Part() {
		if (!OS.isWin32()) {
			return false;
		}
		Path p = path.toAbsolutePath();
		return p.toString().length() <= 3;
	}
	
}
